﻿using System;
using System.ComponentModel.DataAnnotations;
using System.Linq;
using System.Reflection;
using System.Text;
using Db4objects.Db4o;
using Db4objects.Db4o.Config;
using Db4objects.Db4o.Config.Attributes;
using Db4objects.Db4o.Constraints;
using Db4objects.Db4o.Events;
using Db4objects.Db4o.Internal;
using Db4objects.Db4o.Linq;
using Db4objects.Db4o.Query;
using DonNicky.Db.Db4o.Config.Attributes;

namespace DonNicky.Db.Db4o
{
    public static class IObjectContainerExtensions
    {
        public static void UseExtensions(this IObjectContainer container)
        {
            IEventRegistry events = EventRegistryFactory.ForObjectContainer(container);
            events.Committing += CheckUniqueGroupsConstraints;
            events.Creating += ValidateObject;
            events.Updating += ValidateObject;
            events.Creating += CheckConsistency;
            events.Updating += CheckConsistency;
        }

        static void CheckUniqueGroupsConstraints(object sender, CommitEventArgs e)
        {
            foreach (LazyObjectReference loref in e.Added)
                CheckObjectUniqueGroupsConstraints(loref.GetObject(), e.ObjectContainer());
            foreach (LazyObjectReference loref in e.Updated)
                CheckObjectUniqueGroupsConstraints(loref.GetObject(), e.ObjectContainer());
        }

        private static void ValidateObject(object sender, CancellableObjectEventArgs eventInfo)
        {
            ValidationContext context = new ValidationContext(eventInfo.Object, null, null);
            // This throws when the object isn't valid.
            Validator.ValidateObject(eventInfo.Object, context, true);
        }

        private static void CheckObjectUniqueGroupsConstraints(Object o, IObjectContainer container)
        {
            var res = from f in o.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
                      where f.IsDefined(typeof(UniqueGroupAttribute), true)
                      group f by (f.GetCustomAttributes(typeof(UniqueGroupAttribute), true).Single() as UniqueGroupAttribute).GroupID into uniqueGroup
                      select new { GroupID = uniqueGroup.Key, GroupFields = uniqueGroup };
            foreach (var uniqueGroup in res)
            {
                IQuery query = container.Query();
                query.Constrain(o.GetType());
                if (o.GetType().IsSubclassOf(typeof(Db4oTPEntity)))
                    query.Descend("_deleted").Constrain(false);
                StringBuilder groupName = new StringBuilder();
                foreach (var field in uniqueGroup.GroupFields)
                {
                    query.Descend(field.Name).Constrain(field.GetValue(o));
                    if (groupName.Length > 0)
                        groupName.Append("|");
                    groupName.Append(field.Name);
                }
                int numOfFoundDuplicates = query.Execute().Count;
                if (numOfFoundDuplicates > 1)
                    throw new UniqueFieldValueConstraintViolationException(o.GetType().Name, groupName.ToString());
            }
        }

        private static void CheckConsistency(object sender, CancellableObjectEventArgs eventInfo)
        {
            Object o = eventInfo.Object;
            MethodInfo m = o.GetType().GetMethod("CheckConsistency");
            if (m != null && m.ReturnType == typeof(bool) && (m.GetParameters() == null || m.GetParameters().Length == 0))
            {
                if ((bool)m.Invoke(o, null) == false)
                    throw new ValidationException(String.Format("Inconsistent object state: {0} ({1})", o.GetType().Name, o.ToString()));
            }
        }

        public static void Store(this IObjectContainer container, object[] newObjects)
        {
            foreach (object newObject in newObjects)
                container.Store(newObject);
        }

        public static void DeleteCascadeAll(this IObjectContainer container)
        {
            var deletedObjects = from IDeletable o in container
                                 where o.IsDeleted == true
                                 select o;
            foreach (var deletedObject in deletedObjects)
                container.DeleteCascade(deletedObject);
        }

        public static void DeleteCascade(this IObjectContainer container, IDeletable obj)
        {
            var dependencies = from IDeletable o in container
                               where o.IsDeleted == false &&
                                     o.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(f => f.GetValue(o) == obj && f.IsDefined(typeof(DeleteStrategyAttribute), true) && (f.GetCustomAttributes(typeof(DeleteStrategyAttribute), true).Single() as DeleteStrategyAttribute).Behavior == DeleteBehavior.Cascade).Count() > 0
                               select o;
            if (dependencies != null)
                foreach (var dependency in dependencies)
                {
                    dependency.Delete();
                    container.DeleteCascade(dependency);
                }
        }

        public static void Purge(this IObjectContainer container)
        {
            var deletedObjects = from IDeletable o in container
                                 where o.IsDeleted == true
                                 select o;
            foreach (var deletedObject in deletedObjects)
            {
                var dependencies = from IDeletable o in container
                                   where o.IsDeleted == false &&
                                         o.GetType().GetFields(BindingFlags.NonPublic | BindingFlags.Instance).Where(f => f.GetValue(o) == deletedObject &&
                                                                                                                     (!f.IsDefined(typeof(DeleteStrategyAttribute), true) || (f.IsDefined(typeof(DeleteStrategyAttribute), true) && (f.GetCustomAttributes(typeof(DeleteStrategyAttribute), true).Single() as DeleteStrategyAttribute).Behavior != DeleteBehavior.SetNull))).Count() > 0
                                   select o;
                if (dependencies == null || dependencies.Count() == 0)
                {
                    if (deletedObject is Db4oTPEntity)
                        (deletedObject as Db4oTPEntity).Purging = true;
                    container.Delete(deletedObject);
                }
            }
        }
    }
}
